package org.batfish.specifier.parboiled;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import javax.annotation.ParametersAreNonnullByDefault;
import org.batfish.specifier.Grammar;
import org.batfish.specifier.NameNodeSpecifier;
import org.batfish.specifier.NameRegexNodeSpecifier;
import org.batfish.specifier.NodeSpecifier;
import org.batfish.specifier.RoleNameNodeSpecifier;
import org.batfish.specifier.SpecifierContext;
import org.batfish.specifier.TypesNodeSpecifier;
import org.parboiled.errors.InvalidInputError;
import org.parboiled.parserunners.ReportingParseRunner;
import org.parboiled.support.ParsingResult;

/** An {@link NodeSpecifier} that resolves based on the AST generated by {@link Parser}. */
@ParametersAreNonnullByDefault
public final class ParboiledNodeSpecifier implements NodeSpecifier {

  @ParametersAreNonnullByDefault
  private final class NodeAstNodeToNodes implements NodeAstNodeVisitor<Set<String>> {
    private final SpecifierContext _ctxt;

    NodeAstNodeToNodes(SpecifierContext ctxt) {
      _ctxt = ctxt;
    }

    @Override
    public Set<String> visitDifferenceNodeAstNode(DifferenceNodeAstNode differenceNodeAstNode) {
      return Sets.difference(
          differenceNodeAstNode.getLeft().accept(this),
          differenceNodeAstNode.getRight().accept(this));
    }

    @Override
    public Set<String> visitIntersectionNodeAstNode(
        IntersectionNodeAstNode intersectionNodeAstNode) {
      return Sets.intersection(
          intersectionNodeAstNode.getLeft().accept(this),
          intersectionNodeAstNode.getRight().accept(this));
    }

    @Override
    public Set<String> visitNameNodeAstNode(NameNodeAstNode nameNodeAstNode) {
      return new NameNodeSpecifier(nameNodeAstNode.getName()).resolve(_ctxt);
    }

    @Override
    public Set<String> visitNameRegexNodeAstNode(NameRegexNodeAstNode nameRegexNodeAstNode) {
      return new NameRegexNodeSpecifier(nameRegexNodeAstNode.getPattern()).resolve(_ctxt);
    }

    @Override
    public Set<String> visitRoleNodeAstNode(RoleNodeAstNode roleNodeAstNode) {
      // Because we changed the input on June 8 2019 from (role, dim) to (dim, role), we
      // first interpret the user input as (dim, role) if the dim exists. Otherwise, we interpret
      // it is as (role, dim)
      if (_ctxt.getNodeRoleDimension(roleNodeAstNode.getDimensionName()).isPresent()) {
        return new RoleNameNodeSpecifier(
                roleNodeAstNode.getRoleName(), roleNodeAstNode.getDimensionName())
            .resolve(_ctxt);
      } else if (_ctxt.getNodeRoleDimension(roleNodeAstNode.getRoleName()).isPresent()) {
        return new RoleNameNodeSpecifier(
                roleNodeAstNode.getDimensionName(), roleNodeAstNode.getRoleName())
            .resolve(_ctxt);
      }
      throw new NoSuchElementException(
          "Node role dimension " + roleNodeAstNode.getDimensionName() + " does not exist.");
    }

    @Override
    public Set<String> visitTypeNodeAstNode(TypeNodeAstNode typeNodeAstNode) {
      return new TypesNodeSpecifier(ImmutableSet.of(typeNodeAstNode.getDeviceType()))
          .resolve(_ctxt);
    }

    @Override
    public Set<String> visitUnionNodeAstNode(UnionNodeAstNode unionNodeAstNode) {
      return Sets.union(
          unionNodeAstNode.getLeft().accept(this), unionNodeAstNode.getRight().accept(this));
    }
  }

  private final NodeAstNode _ast;

  ParboiledNodeSpecifier(NodeAstNode ast) {
    _ast = ast;
  }

  /**
   * Returns an {@link NodeSpecifier} based on {@code input} which is parsed as {@link
   * Grammar#NODE_SPECIFIER}.
   *
   * @throws IllegalArgumentException if the parsing fails or does not produce the expected AST
   */
  public static ParboiledNodeSpecifier parse(String input) {
    return new ParboiledNodeSpecifier(getAst(input));
  }

  static NodeAstNode getAst(String input) {
    ParsingResult<AstNode> result =
        new ReportingParseRunner<AstNode>(Parser.instance().getInputRule(Grammar.NODE_SPECIFIER))
            .run(input);

    if (!result.parseErrors.isEmpty()) {
      throw new IllegalArgumentException(
          ParserUtils.getErrorString(
              input,
              Grammar.NODE_SPECIFIER,
              (InvalidInputError) result.parseErrors.get(0),
              Parser.ANCHORS));
    }

    AstNode ast = ParserUtils.getAst(result);
    checkArgument(
        ast instanceof NodeAstNode, "Unexpected AST for nodeSpec input '%s': '%s'", input, ast);

    return (NodeAstNode) ast;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof ParboiledNodeSpecifier)) {
      return false;
    }
    return Objects.equals(_ast, ((ParboiledNodeSpecifier) o)._ast);
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(_ast);
  }

  @Override
  public Set<String> resolve(SpecifierContext ctxt) {
    return _ast.accept(new NodeAstNodeToNodes(ctxt));
  }
}
